package com.pricl.frame.page.paging;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.PreparedStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;

import com.pricl.frame.common.utils.ReflectUtils;
import com.pricl.frame.page.module.Page;

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }),
	@Signature(type = StatementHandler.class, method = "query", args = { Statement.class, ResultHandler.class }) })
public class PagingPlugin implements Interceptor {

private static final Log log = LogFactory.getLog(PagingPlugin.class);

private IPagingAdapter adapter;

protected Page<?> seekPage(Object parameter) {
	Page<?> page = null;
	if (parameter == null) {
		return null;
	}
	if (parameter instanceof Page) {
		page = (Page<?>) parameter;
	}
	else if (parameter instanceof Map) {
		@SuppressWarnings("rawtypes")
		Map map = (Map) parameter;
		for (Object arg : map.values()) {
			if (arg instanceof Page) {
				page = (Page<?>) arg;
				break;
			}
		}
	}
	return page;
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Object intercept(Invocation ivk) throws Throwable {
	PreparedStatementHandler delegate = (PreparedStatementHandler) ReflectUtils.fieldValue(ivk.getTarget(), "delegate");
	MappedStatement mappedStatement = (MappedStatement) ReflectUtils.fieldValue(delegate, "mappedStatement");
	if (delegate == null || mappedStatement == null) {
		return ivk.proceed();
	}
	BoundSql boundSql = delegate.getBoundSql();
	Object parameterObject = boundSql.getParameterObject();
	Page<?> page = seekPage(parameterObject);
	if (page == null) {
		return ivk.proceed();
	}
	if (page.pageSize <= 0 || page.pageNumber <= 0 || page.notPage) {
		return ivk.proceed();
	}
	if (ivk.getArgs()[0] instanceof Connection) {
		Configuration configuration = mappedStatement.getConfiguration();
		// 查询总数
		page.total = getTotalSize(ivk, configuration, mappedStatement, boundSql, parameterObject);
		log.debug("分页查询总记录数:" + page.total);
		// 修改SQL
		String newSql = adapter.paging(boundSql.getSql(), page);
		log.debug("分页查询语句:\r\n" + newSql);
		ReflectUtils.putFieldValue(boundSql, "sql", newSql);
		return ivk.proceed();
	}
	// 查询结果集
	return page.rows = (List) ivk.proceed();
}

/**
 * <p>
 * 格式化总数SQL
 * </p>
 * 
 * @param sql
 * @return String
 */
private static String formatTotalSql(String sql) {
	if (Pattern.compile("\\sgroup\\sby\\s", Pattern.CASE_INSENSITIVE).matcher(sql).find()) {
		return "select count(*) as total from (" + sql + ") records";
	}
	Matcher orderByPtn = Pattern.compile("\\sorder\\sby\\s.*$", Pattern.CASE_INSENSITIVE).matcher(sql);
	if (orderByPtn.find()) {
		sql = orderByPtn.replaceAll("");
	}
	String[] forms = Pattern.compile("\\sfrom\\s", Pattern.CASE_INSENSITIVE).split(sql);
	Pattern selectPtn = Pattern.compile("(\\s|,|\\(|^)select\\s", Pattern.CASE_INSENSITIVE);
	int selectSize = 0;
	int fromIndex = -1;
	for (int i = 0; i < forms.length - 1; i++) {
		selectSize += selectPtn.split(forms[i]).length - 1;
		if (selectSize == 1) {
			fromIndex = i + 1;
			break;
		}
		selectSize -= 1;
	}
	if (fromIndex == -1) {
		fromIndex = forms.length - 1;
	}
	StringBuffer formatSql = new StringBuffer();
	formatSql.append("select count(*) as total");
	for (int i = fromIndex; i < forms.length; i++) {
		formatSql.append(" from ").append(forms[i]);
	}
	return formatSql.toString();
}

/**
 * <p>
 * 获取记录数
 * </p>
 * 
 * @param ivk
 * @param configuration
 * @param mappedStatement
 * @param boundSql
 * @param parameterObject
 * @return
 * @throws SQLException
 */
public static void main(String[] args) {
	String totalSql = "select * from mysql where xxx and order by 1";
	int ofIgnoreCase = StringUtils.lastIndexOfIgnoreCase(totalSql, "order by");
	if (ofIgnoreCase != -1) {
		System.out.println(StringUtils.substring(totalSql, ofIgnoreCase));
		System.out.println(totalSql);
		System.out.println(StringUtils.remove(totalSql, StringUtils.substring(totalSql, ofIgnoreCase)));
	}
}
private int getTotalSize(Invocation ivk, Configuration configuration, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException {
	Connection conn = (Connection) ivk.getArgs()[0];
	PreparedStatement pstmt = null;
	ResultSet rs = null;
	int totalSize = 0;
	try {
		String totalSql = formatTotalSql(boundSql.getSql());
		
		// add by winton 2017-9-13
		// 修改查询分页总数的语句。部分sql由于orderby导致索引失效，导致执行total的sql耗时严重的问题。去除orderby
		int ofIgnoreCase = StringUtils.lastIndexOfIgnoreCase(totalSql, "order by");
		if (ofIgnoreCase != -1) {
			totalSql = StringUtils.remove(totalSql, StringUtils.substring(totalSql, ofIgnoreCase));
		}
		
		ParameterHandler handler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
		pstmt = conn.prepareStatement(totalSql);
		handler.setParameters(pstmt);
		rs = pstmt.executeQuery();
		if (rs.next()) {
			totalSize = rs.getInt(1);
		}
	}
	finally {
		if (rs != null) {
			rs.close();
		}
		if (pstmt != null) {
			pstmt.close();
		}
	}
	return totalSize;
}

@Override
public Object plugin(Object target) {
	if (target instanceof StatementHandler) {
		return Plugin.wrap(target, this);
	}
	return target;
}

@Override
public void setProperties(Properties p) {
	String adapter = p.getProperty("adapter");
	IPagingAdapter pa = null;
	if (adapter != null && !(adapter = adapter.trim()).isEmpty()) {
		try {
			pa = (IPagingAdapter) Class.forName(adapter).newInstance();
		}
		catch (Exception e) {
			log.error("无法解析分页适配器", e);
		}
	}
	this.adapter = pa != null ? pa : new MysqlPagingAdapter();
}
}

